package vsy.buzzwordbingo.model.impl;

import java.math.BigInteger;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import vsy.buzzwordbingo.model.DuplicatePlayerException;
import vsy.buzzwordbingo.model.Game;
import vsy.buzzwordbingo.model.GameBoard;
import vsy.buzzwordbingo.model.GameCore;
import vsy.buzzwordbingo.model.Player;

public class GameCoreImpl implements GameCore {
	
	/* Default time out value in millis*/
	private static long TIME_OUT = 20000;

	private ConcurrentMap<String, Player> players;
	private ConcurrentMap<String, Game> games;
	
	private static final BigInteger ONE = new BigInteger("1");
	
	public GameCoreImpl() {
		players = new ConcurrentHashMap<String, Player>();
		games = new ConcurrentHashMap<String, Game>();
	}
	
	@Override 
	public void loginPlayer(Player player) throws DuplicatePlayerException {
		//ensure player name contains only letters and underscore
		//TODO figure out how to handle exception
//			for (int i = 0; i < name.length(); i++){
//				if ('a' <= name.charAt(i) && name.charAt(i) <= 'z');
//				else if ('A' <= name.charAt(i) && name.charAt(i) <= 'Z');
//				else if (name.charAt(i) == '_');
//				else throw new IllegalArgumentException("player name may only contain letters and underscore");
//			}
		
		if (players.putIfAbsent(player.getName(), player) != null) {
			throw new DuplicatePlayerException(player.getName());
		}
	}
	
	@Override
	public Player loginPlayer(String name, String password) throws DuplicatePlayerException, IllegalArgumentException {
		Player newPlayer = new PlayerImpl(name, password);
		loginPlayer(newPlayer);
		return newPlayer;
	}

	@Override
	public void logoutPlayer(String name) {
		List<Game> games = new ArrayList<Game>();
		games.addAll(getPlayer(name).getGames());
		for (Game game:games)
			leaveGame(game.getId(), name);
		players.remove(name);
		
	}

	@Override
	public Game getGame(String gameId) {
		return games.get(gameId);
	}

	@Override
	public void leaveGame(String gameId, String playerName) {
		Player player = players.get(playerName);
		Game game = games.get(gameId);
		game.leaveGame(player);
		System.out.println("Player \"" + playerName + "\" has left the Game " + gameId);
	}
	
	/**
	 * Cleans up the list of players for all active games.<br>
	 * Checks if any player timed out in any game and removes<br>
	 * all timed out players from its games.
	 * */
	public void cleanupPlayerLists() {
		for (Game currentGame : games.values()) {
			for (GameBoard board: currentGame.getBoards()) {
				if ((System.currentTimeMillis() - board.getLastPollTime()) > TIME_OUT) {
					Player player = board.getPlayer();
					currentGame.kick(player);
				}
			}
		}
		System.out.println("called for cleanup");
	}

	/**
	 * Creates an ArrayList of all active games and returns it.*/
	@Override
	public List<Game> getGames() {
		List<Game> gameList = new ArrayList<Game>();
		gameList.addAll(games.values());
		return gameList;
	}

	@Override
	public Game createGame(String playerName, String title, int size, String description, List<String> words) {
		Player player = players.get(playerName);
		if (player != null) {
				String normalizedTitle = normalizeTitle(title);
				String id = normalizedTitle;
				BigInteger postfix = ONE;
				Game newGame;
				while (games.putIfAbsent(id, newGame = new GameImpl(id, title, size, description, player, words)) != null) {
					postfix = findPostfixForUniqueKey(normalizedTitle, postfix);
					id = createPostfixedKey(normalizedTitle, postfix);
				}
				joinGame(newGame.getId(), playerName);
				return newGame;
		}
		return null;
	}

	private String normalizeTitle(String title) {
        String normalizedTitle = Normalizer.normalize(title, Normalizer.Form.NFD);
        return normalizedTitle.replaceAll("[^\\w\\d]", " ").trim().replaceAll("\\s+", "_");
	}

	private BigInteger findPostfixForUniqueKey(String key, BigInteger startPostfix) {
		String possiblyUniqueKey = createPostfixedKey(key, startPostfix);
		BigInteger postfix = startPostfix;
		while (games.containsKey(possiblyUniqueKey)) {
			postfix.add(ONE);
			possiblyUniqueKey = createPostfixedKey(possiblyUniqueKey, postfix);
		}
		return postfix;
	}
	
	private String createPostfixedKey(String key, BigInteger postfix) {
		return key + "_" + postfix.toString();
	}

	@Override
	public void markBuzzword(String gameId, String playerName, int row, int col, boolean marked) {
		getBoard(gameId, playerName).setMarked(row, col, marked);
	}

	@Override
	public List<Player> getPlayers() {
		List<Player> playerList = new ArrayList<Player>();
		playerList.addAll(players.values());
		return playerList;
	}

	@Override
	public Player getPlayer(String playerName) {
		return players.get(playerName);
	}

	@Override
	public void destroyGame(String gameId) {
		Game game = games.get(gameId);
		new GarbageCollector(game);

	}

	@Override
	public GameBoard joinGame(String gameId, String playerName) {
		Player player = players.get(playerName);
		Game game = games.get(gameId);
		GameBoard board = game.joinGame(player);
		return board;
	}

	@Override
	public GameBoard getBoard(String gameId, String playerName) {
		Game game = getGame(gameId);
		Player player = getPlayer(playerName);
		
		//Use player to find board, since it is less likely for a player to have multiple
		//games than it is for a game to have multiple players. Usually one player will only
		//participate in one game, so the for loop should take constant time.
		System.out.println(player.getBoards());
		for (GameBoard board : player.getBoards()) {
			if (board.getGame().equals(game)) {
				return board;
			}
		}
		return null;
	}
	
	@Override
	public void poll(String gameId, String playerName) {
		// Updates the last poll time of a specific player in a specific game
		GameBoard board = getBoard(gameId, playerName);
		if (board != null)
			board.updateLastPollTime();
	}
	
	private class GarbageCollector extends Thread {
		
		private Game game;
		
		public GarbageCollector(Game game) {
			this.game = game;
			start();
		}
				
		@Override
		public void run() {
			try {
				sleep(10000);
			} catch (InterruptedException ignored) {
			}
			for (Player player : game.getPlayers()) {
				//entfernt den jeweiligen Spieler und dessen zugehöriges Spielfeld
				game.kick(player);
			}
			games.remove(game);
		}
	}
}
